#  This file is part of OctoBot (https://github.com/Drakkar-Software/OctoBot)
#  Copyright (c) 2025 Drakkar-Software, All rights reserved.
#
#  OctoBot is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either
#  version 3.0 of the License, or (at your option) any later version.
#
#  OctoBot is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#  General Public License for more details.
#
#  You should have received a copy of the GNU General Public
#  License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.
import logging
import logging.config as config
import os
import shutil
import traceback

import sys
import async_channel.channels as channel_instances
import async_channel.enums as channel_enums

import octobot_commons.enums as enums
import octobot_commons.constants as commons_constants
import octobot_commons.logging as common_logging
import octobot_commons.channels_name as channels_name
import octobot_commons.pretty_printer as pretty_printer

import octobot_evaluators.evaluators.channel as evaluator_channels

import octobot_trading.exchange_channel as exchanges_channel
import octobot_trading.enums as trading_enums
import octobot_trading.api as trading_api
import octobot_trading.personal_data as personal_data

import octobot.constants as constants
import octobot.configuration_manager as configuration_manager

BOT_CHANNEL_LOGGER = None
LOGGER_PRIORITY_LEVEL = channel_enums.ChannelConsumerPriorityLevels.OPTIONAL.value


def _log_uncaught_exceptions(ex_cls, ex, tb):
    logging.exception("".join(traceback.format_tb(tb)))
    logging.exception("{0}: {1}".format(ex_cls, ex))


def init_logger():
    try:
        if not os.path.exists(constants.LOGS_FOLDER):
            os.mkdir(constants.LOGS_FOLDER)
        _load_logger_config()
        init_bot_channel_logger()
    except KeyError:
        print(
            "Impossible to start OctoBot: the logging configuration can't be found in '"
            + constants.LOGGING_CONFIG_FILE
            + "' please make sure you are running OctoBot from its root directory."
        )
        os._exit(-1)

    logger = logging.getLogger("OctoBot Launcher")

    try:
        # Force new log file creation not to log at the previous one's end.
        logger.parent.handlers[1].doRollover()
    except PermissionError:
        print(
            "Impossible to start OctoBot: the logging file is locked, this is probably due to another running "
            "OctoBot instance."
        )
        os._exit(-1)

    sys.excepthook = _log_uncaught_exceptions
    return logger


def init_bot_channel_logger():
    # overwrite BOT_CHANNEL_LOGGER to apply global logging configuration
    global BOT_CHANNEL_LOGGER
    BOT_CHANNEL_LOGGER = common_logging.get_logger("OctoBot Channel")


def _load_logger_config():
    try:
        # use local logging file to allow users to customize the log level
        if not os.path.isfile(configuration_manager.get_user_local_config_file()):
            if not os.path.exists(commons_constants.USER_FOLDER):
                os.mkdir(commons_constants.USER_FOLDER)
            shutil.copyfile(constants.LOGGING_CONFIG_FILE, configuration_manager.get_user_local_config_file())
        config.fileConfig(configuration_manager.get_user_local_config_file())
        if constants.FORCED_LOG_LEVEL:
            logging.getLogger("Logging Configuration").info(
                f"Applying forced logging level {constants.FORCED_LOG_LEVEL}"
            )
            common_logging.set_global_logger_level(constants.FORCED_LOG_LEVEL)
    except Exception as ex:
        config.fileConfig(constants.LOGGING_CONFIG_FILE)
        logging.getLogger("Logging Configuration").warning(
            f"Impossible to initialize local logging configuration file, using default one. {ex}"
        )


async def init_exchange_chan_logger(exchange_id):
    await exchanges_channel.get_chan(channels_name.OctoBotTradingChannelsName.OHLCV_CHANNEL.value,
                                     exchange_id).new_consumer(
        ohlcv_callback, priority_level=LOGGER_PRIORITY_LEVEL
    )
    await exchanges_channel.get_chan(channels_name.OctoBotTradingChannelsName.BALANCE_CHANNEL.value,
                                     exchange_id).new_consumer(
        balance_callback, priority_level=channel_enums.ChannelConsumerPriorityLevels.MEDIUM.value
    )
    await exchanges_channel.get_chan(channels_name.OctoBotTradingChannelsName.TRADES_CHANNEL.value,
                                     exchange_id).new_consumer(
        trades_callback, priority_level=channel_enums.ChannelConsumerPriorityLevels.MEDIUM.value
    )
    await exchanges_channel.get_chan(channels_name.OctoBotTradingChannelsName.LIQUIDATIONS_CHANNEL.value,
                                     exchange_id).new_consumer(
        liquidations_callback, priority_level=channel_enums.ChannelConsumerPriorityLevels.MEDIUM.value
    )
    await exchanges_channel.get_chan(channels_name.OctoBotTradingChannelsName.POSITIONS_CHANNEL.value,
                                     exchange_id).new_consumer(
        positions_callback, priority_level=channel_enums.ChannelConsumerPriorityLevels.MEDIUM.value
    )
    await exchanges_channel.get_chan(channels_name.OctoBotTradingChannelsName.ORDERS_CHANNEL.value,
                                     exchange_id).new_consumer(
        orders_callback, priority_level=channel_enums.ChannelConsumerPriorityLevels.MEDIUM.value
    )
    # secondary logs, very verbose on websockets
    if constants.ENV_TRADING_ENABLE_DEBUG_LOGS:
        await exchanges_channel.get_chan(channels_name.OctoBotTradingChannelsName.RECENT_TRADES_CHANNEL.value,
                                         exchange_id).new_consumer(
            recent_trades_callback, priority_level=LOGGER_PRIORITY_LEVEL
        )
        await exchanges_channel.get_chan(channels_name.OctoBotTradingChannelsName.FUNDING_CHANNEL.value,
                                         exchange_id).new_consumer(
            funding_callback, priority_level=LOGGER_PRIORITY_LEVEL
        )
        await exchanges_channel.get_chan(channels_name.OctoBotTradingChannelsName.TICKER_CHANNEL.value,
                                         exchange_id).new_consumer(
            ticker_callback, priority_level=LOGGER_PRIORITY_LEVEL
        )
        await exchanges_channel.get_chan(channels_name.OctoBotTradingChannelsName.MINI_TICKER_CHANNEL.value,
                                         exchange_id).new_consumer(
            mini_ticker_callback, priority_level=LOGGER_PRIORITY_LEVEL
        )
        await exchanges_channel.get_chan(channels_name.OctoBotTradingChannelsName.ORDER_BOOK_CHANNEL.value,
                                         exchange_id).new_consumer(
            order_book_callback, priority_level=LOGGER_PRIORITY_LEVEL
        )
        await exchanges_channel.get_chan(channels_name.OctoBotTradingChannelsName.ORDER_BOOK_TICKER_CHANNEL.value,
                                         exchange_id).new_consumer(
            order_book_ticker_callback, priority_level=LOGGER_PRIORITY_LEVEL
        )
        await exchanges_channel.get_chan(channels_name.OctoBotTradingChannelsName.KLINE_CHANNEL.value,
                                         exchange_id).new_consumer(
            kline_callback, priority_level=LOGGER_PRIORITY_LEVEL
        )
        await exchanges_channel.get_chan(channels_name.OctoBotTradingChannelsName.MARK_PRICE_CHANNEL.value,
                                         exchange_id).new_consumer(
            mark_price_callback, priority_level=LOGGER_PRIORITY_LEVEL
        )
        await exchanges_channel.get_chan(channels_name.OctoBotTradingChannelsName.BALANCE_PROFITABILITY_CHANNEL.value,
                                         exchange_id).new_consumer(
            balance_profitability_callback, priority_level=channel_enums.ChannelConsumerPriorityLevels.MEDIUM.value
        )


async def init_evaluator_chan_logger(matrix_id: str):
    await evaluator_channels.get_chan(channels_name.OctoBotEvaluatorsChannelsName.MATRIX_CHANNEL.value,
                                      matrix_id).new_consumer(
        matrix_callback, priority_level=LOGGER_PRIORITY_LEVEL
    )
    await evaluator_channels.get_chan(channels_name.OctoBotEvaluatorsChannelsName.EVALUATORS_CHANNEL.value,
                                      matrix_id).new_consumer(
        evaluators_callback, priority_level=LOGGER_PRIORITY_LEVEL
    )


async def init_octobot_chan_logger(bot_id: str):
    return await channel_instances.get_chan_at_id(constants.OCTOBOT_CHANNEL, bot_id).new_consumer(
        octobot_channel_callback,
        priority_level=LOGGER_PRIORITY_LEVEL,
        bot_id=bot_id,
        subject=[enums.OctoBotChannelSubjects.NOTIFICATION.value, enums.OctoBotChannelSubjects.ERROR.value]
    )


async def ticker_callback(
        exchange: str, exchange_id: str, cryptocurrency: str, symbol: str, ticker
):
    BOT_CHANNEL_LOGGER.debug(
        f"TICKER : EXCHANGE = {exchange} || CRYPTOCURRENCY = {cryptocurrency} "
        f"|| SYMBOL = {symbol} || TICKER = {ticker}"
    )


async def mini_ticker_callback(
        exchange: str, exchange_id: str, cryptocurrency: str, symbol: str, mini_ticker
):
    BOT_CHANNEL_LOGGER.debug(
        f"MINI TICKER : EXCHANGE = {exchange} || CRYPTOCURRENCY = {cryptocurrency} "
        f"|| SYMBOL = {symbol} || MINI TICKER = {mini_ticker}"
    )


async def order_book_callback(
        exchange: str, exchange_id: str, cryptocurrency: str, symbol: str, asks, bids
):
    BOT_CHANNEL_LOGGER.debug(
        f"ORDERBOOK : EXCHANGE = {exchange} || CRYPTOCURRENCY = {cryptocurrency} "
        f"|| SYMBOL = {symbol} || ASKS = {len(asks)} orders || BIDS = {len(bids)} orders"
    )


async def order_book_ticker_callback(
        exchange: str,
        exchange_id: str,
        cryptocurrency: str,
        symbol: str,
        ask_quantity,
        ask_price,
        bid_quantity,
        bid_price,
):
    BOT_CHANNEL_LOGGER.debug(
        f"ORDERBOOK TICKER : EXCHANGE = {exchange} || SYMBOL = {symbol} "
        f"|| ASK PRICE / QUANTIY = {ask_price} / {ask_quantity}"
        f"|| BID PRICE / QUANTIY = {bid_price} / {bid_quantity}"
    )


async def ohlcv_callback(
        exchange: str,
        exchange_id: str,
        cryptocurrency: str,
        symbol: str,
        time_frame,
        candle,
):
    BOT_CHANNEL_LOGGER.debug(
        f"OHLCV : EXCHANGE = {exchange} || CRYPTOCURRENCY = {cryptocurrency} || SYMBOL = {symbol} "
        f"|| TIME FRAME = {time_frame} || CANDLE = {candle}"
    )


async def recent_trades_callback(
        exchange: str, exchange_id: str, cryptocurrency: str, symbol: str, recent_trades
):
    BOT_CHANNEL_LOGGER.debug(
        f"RECENT TRADES : EXCHANGE = {exchange} || CRYPTOCURRENCY = {cryptocurrency} "
        f"|| SYMBOL = {symbol} || 10 first RECENT TRADES = {recent_trades[:10]}"
    )


async def liquidations_callback(
        exchange: str, exchange_id: str, cryptocurrency: str, symbol: str, liquidations
):
    BOT_CHANNEL_LOGGER.debug(
        f"LIQUIDATIONS : EXCHANGE = {exchange} || CRYPTOCURRENCY = {cryptocurrency} "
        f"|| SYMBOL = {symbol} || LIQUIDATIONS = {liquidations}"
    )


async def kline_callback(
        exchange: str, exchange_id: str, cryptocurrency: str, symbol: str, time_frame, kline
):
    BOT_CHANNEL_LOGGER.debug(
        f"KLINE : EXCHANGE = {exchange} || CRYPTOCURRENCY = {cryptocurrency} || SYMBOL = {symbol} "
        f"|| TIME FRAME = {time_frame} || KLINE = {kline}"
    )


async def mark_price_callback(
        exchange: str, exchange_id: str, cryptocurrency: str, symbol: str, mark_price
):
    BOT_CHANNEL_LOGGER.debug(
        f"MARK PRICE : EXCHANGE = {exchange} || CRYPTOCURRENCY = {cryptocurrency} "
        f"|| SYMBOL = {symbol} || MARK PRICE = {mark_price}"
    )


def _filter_balance(balance: dict):
    if not balance:
        return balance, 0
    first_value = next(iter(balance.values()))
    if isinstance(first_value, dict):
        filtered_balance = {
            key: values
            for key, values in balance.items()
            if values.get(commons_constants.PORTFOLIO_TOTAL)
        }
        removed_count = len(balance) - len(filtered_balance)
        return trading_api.parse_decimal_portfolio(filtered_balance, False), removed_count
    elif isinstance(first_value, personal_data.Asset):
        filtered_balance = {
            key: values
            for key, values in balance.items()
            if values.total
        }
        removed_count = len(balance) - len(filtered_balance)
        return filtered_balance, removed_count
    return balance, 0


async def balance_callback(exchange: str, exchange_id: str, balance):
    filtered_balance, filtered_count = _filter_balance(balance)
    BOT_CHANNEL_LOGGER.debug(
        f"BALANCE : EXCHANGE = {exchange} || BALANCE = {filtered_balance} ({filtered_count} filtered empty assets)"
    )


async def balance_profitability_callback(
        exchange: str,
        exchange_id: str,
        profitability,
        profitability_percent,
        market_profitability_percent,
        initial_portfolio_current_profitability,
):
    BOT_CHANNEL_LOGGER.debug(
        f"BALANCE PROFITABILITY : EXCHANGE = {exchange} || PROFITABILITY = "
        f"{pretty_printer.portfolio_profitability_pretty_print(profitability, profitability_percent, 'USDT')}"
    )


async def trades_callback(
        exchange: str,
        exchange_id: str,
        cryptocurrency: str,
        symbol: str,
        trade: dict,
        old_trade: bool,
):
    BOT_CHANNEL_LOGGER.debug(
        f"TRADES : EXCHANGE = {exchange} || CRYPTOCURRENCY = {cryptocurrency} || SYMBOL = {symbol} "
        f"|| TRADE = {trade} "
        f"|| OLD_TRADE = {old_trade}"
    )


async def orders_callback(
        exchange: str,
        exchange_id: str,
        cryptocurrency: str,
        symbol: str,
        order: dict,
        update_type: str,
        is_from_bot: bool,
):
    order_string = f"ORDERS : EXCHANGE = {exchange} || SYMBOL = {symbol} || " \
                   f"{pretty_printer.open_order_pretty_printer(exchange, order)} || " \
                   f"status = {order.get(trading_enums.ExchangeConstantsOrderColumns.STATUS.value, None)} || " \
                   f"UPDATE_TYPE = {update_type} || FROM_BOT = {is_from_bot}"
    BOT_CHANNEL_LOGGER.debug(order_string)


async def positions_callback(
        exchange: str,
        exchange_id: str,
        cryptocurrency: str,
        symbol: str,
        position,
        is_updated: bool
):
    BOT_CHANNEL_LOGGER.debug(f"POSITIONS : EXCHANGE = {exchange} || POSITIONS = {position}")


async def funding_callback(
        exchange: str,
        exchange_id: str,
        cryptocurrency: str,
        symbol: str,
        funding_rate,
        predicted_funding_rate,
        next_funding_time,
        timestamp,
):
    BOT_CHANNEL_LOGGER.debug(
        f"FUNDING : EXCHANGE = {exchange} || CRYPTOCURRENCY = {cryptocurrency} || SYMBOL = {symbol} "
        f"|| RATE = {str(funding_rate)} || NEXT RATE = {str(predicted_funding_rate)}"
        f"|| NEXT TIME = {str(next_funding_time)} || TIMESTAMP = {str(timestamp)}"
    )


async def matrix_callback(
        matrix_id,
        evaluator_name,
        evaluator_type,
        eval_note,
        eval_note_type,
        exchange_name,
        cryptocurrency,
        symbol,
        time_frame,
):
    BOT_CHANNEL_LOGGER.debug(
        f"MATRIX : EXCHANGE = {exchange_name} || "
        f"EVALUATOR = {evaluator_name} || EVALUATOR_TYPE = {evaluator_type} || "
        f"CRYPTOCURRENCY = {cryptocurrency} || SYMBOL = {symbol} || TF = {time_frame} "
        f"|| NOTE = {eval_note} [MATRIX id = {matrix_id}] "
    )


async def evaluators_callback(
        matrix_id,
        evaluator_name,
        evaluator_type,
        exchange_name,
        cryptocurrency,
        symbol,
        time_frame,
        data,
):
    BOT_CHANNEL_LOGGER.debug(
        f"EVALUATORS : EXCHANGE = {exchange_name} || "
        f"EVALUATOR = {evaluator_name} || EVALUATOR_TYPE = {evaluator_type} || "
        f"CRYPTOCURRENCY = {cryptocurrency} || SYMBOL = {symbol} || TF = {time_frame} "
        f"|| DATA = {data} [MATRIX id = {matrix_id}] "
    )


async def octobot_channel_callback(
        bot_id: str,
        subject: str,
        action: str,
        data: dict
):
    BOT_CHANNEL_LOGGER.debug(
        f"OCTOBOT_CHANNEL : SUBJECT = {subject} || ACTION = {action} || DATA = {data} "
    )
